前一篇我們初步修改Shader,並且介紹GLSL的型別、函式、程式進入點、程式最終任務。
本篇將繼續介紹:
經由上一篇,有了型別、函式、程式進入點、程式最終任務這些概念之後,我們其實就有能力修改Shader,創造很多效果。
回顧上一篇,我們準備好了開發環境,並且在shader給定了gl_FragColor變數,賦予了球體一個顏色。
有了gl_FragColor,就可以指定球是綠色、紅色、紫色等各種顏色。就像上一篇提到的那樣,我修改R, G, B, A為0.0, 1.0, 0.0, 0.0,那個就可以產生綠色。
gl_FragColor=vec4(0.0, 1.0, 0.0, 1.);

有興趣可以看前一篇文章,它介紹如何修改顏色。但操作過你將會發現一件事情:在Fragment Shader當中雖然可以直接指定顏色,可是我們若沒辦法將一些資料傳到的Fragment Shader,那麼Fragment Shader就很孤單的只能換換顏色而已。
為了活化應用,我們必須從javascript傳值到shader,以下將實作開發,使得用戶可以自己調整球體的顏色。
我準備了上手的程式碼,直接複製貼上即可。幾乎跟前一篇的示例一模一樣。主要就是建置three.js的開發環境,然後新增一顆球。

https://codepen.io/umas-sunavan/pen/xxjBXrB?editors=1010
上一篇,我們實例化了ShaderMaterial,並且指定shader的字串內容為vertex跟fragment,在ShaderMaterial用GLSL編譯,最後在GPU渲染。
const addSphere = () => {
	const vertex = document.getElementById('vertexShader').innerHTML
	const fragment = document.getElementById('fragmentShader').innerHTML
    const geo = new THREE.SphereGeometry(5,15,15)
    const mat = new THREE.ShaderMaterial({
		//  指定vertexShader程式碼來源
		vertexShader: vertex,
		//  指定fragmentShader程式碼來源
		fragmentShader: fragment,
	})
    const mesh = new THREE.Mesh(geo, mat)
    scene.add(mesh)
    return mesh
}
addSphere()
在HTML,我給了字串,分別代表Vertex Shader跟Fragment Shader,但我還沒有介紹其原理。
<p id="fragmentShader">
    void main(void){
    gl_FragColor=vec4(0.6, 0.3, 1.0, 1.0);
    }
</p>
<p id="vertexShader">
    void main(void){
    gl_Position=projectionMatrix*modelViewMatrix*vec4(position, 1.0);
    }
</p>
目前,球體的顏色長這樣:

接著我們試試看再進階一點的實作:
在HTML加上Range,使得滑鼠可以變更亮度
<main>
+ <input type="range" min="0" max="4" step="0.1" value="0.5" id="intensity">
</main>
首先,在shader加上uIntensity,我將在下一篇提到uniforms 的功用,我先將重點放在傳值。
const mat = new THREE.ShaderMaterial({
		vertexShader: vertex,
		fragmentShader: fragment,
+		uniforms: {
+			uIntensity: {
+				value: 0.5 // 實作下一步input事件之前先給它預設值
+			}
+		}
	})
在javascript單向綁定值到intensity
+ document.getElementById('intensity').addEventListener( 'input', event => {
+ 	sphere.material.uniforms.uIntensity.value = +event.target.value
+ })
接著,就可以看到我們的ambient light做好了。基本上,速成的環境光就這樣完成了。事實上,three.js 的環境光AmbientLight 就是在shader中加上一個intensity來控制整個物件的顏色。
.gif)

https://codepen.io/umas-sunavan/pen/JjvzrMq
如果你重看前面時做的第二點,你會發現,我們使用傳送了intensity ,使得我們可以調整物體亮度,如下面的程式碼,three.js可以傳送數值0.5給shader的變數uIntensity:
const mat = new THREE.ShaderMaterial({
		vertexShader: vertex,
		fragmentShader: fragment,
+		uniforms: {
+			uIntensity: {
+				value: 0.5
+			}
+		}
	})
但我留了幾個伏筆:
uniforms是什麼?為什麼我們得加上這個東西?這是three.js專屬的撰寫方式嗎?還是Shader的撰寫方式?uItensity 就能夠在shader宣告變數為0.5?這個名稱可以自己自定義嗎?如果是,那為什麼要加上u前綴?接著就來回答這些問題。
如它的名字,uniform核心概念就是一致。無論vertex shader在那一幀所執行的錨點有多少的,到第幾個了,也無論fragment shader那一幀所執行像素到底幾顆,uniform的數值那幀都不會變。
這也是為什麼,我們過uniform傳入資料。而這也代表,事實上有些變數行為不同,例如重要的變數類型:
Const就很像Js ES6的Const。至於Define,則是用來取代文件中提及的所有關鍵字,主要是替換文字,基本上並沒有因此多儲存資料在記憶體,而比較像是單純置換所有關鍵字而已,所以能提升效率。
可以從JS傳值到Shader中。
由於GPU在渲染畫面時,一個螢幕會有很多平行的thread一起渲染,但無論是哪一個thread在渲染,這個數值固定一致,這也是被稱作uniforms的原因。
透過其他套件,我們能夠從js傳值到Shader。
例如three.js將uniform傳值的方法:
const mat = new THREE.ShaderMaterial({
		vertexShader: vertex,
		fragmentShader: fragment,
		uniforms: {
			uIntensity: {
				value: 0.5
			}
		}
	})
如此一來,就能在Shader中存取該值
// 在fragment shader,重新宣告uIntensity為uniform即可
uniform float uIntensity;
void main(void){
gl_FragColor=vec4(uIntensity * 1.0, uIntensity * 1.0, uIntensity * 0.0, 1.0);
}
又例如p5.js的作法:
theShader.setUniform("u_resolution", [width, height]); // 傳螢幕大小給shader
接著在Fragment宣告同名的名稱,即可從Js接收資料並且加以應用。
uniform vec2 u_resolution;
本篇從實作環境光開始,快速建立一個shader案例。在這個案例裡面,我們從javascript傳遞數值到了shader。在傳遞的過程中,發現了陌生的關鍵字uniform,並且經過uniform,帶出其它GLSL類別。
經過本篇跟上一篇的說明,現在我們可以自由的從javascript傳送數值到fragment shader,藉此有了進階的操作。
下一篇,我們將透過數值,建立一個光點。